Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Round out --use scenario (add "use" sub-command, add local file, add .bacpac / .mdf & .ldf / compressed file support) #319

Draft
wants to merge 41 commits into
base: main
Choose a base branch
from

Conversation

stuartpa
Copy link
Collaborator

@stuartpa stuartpa commented Apr 6, 2023

DRAFT: Not ready for code review yet.


This PR rounds out the "use" scenario for containers, to allow the casual user who doesn't know containers/linux well, to be able to get going quickly with already existing databases:

  1. --use now supports local and remote files
    sqlcmd create mssql --use [<local file>|<remote file>]

The T/SQL database name by default becomes the --use file name (without extension). This can be overridden with a ",", e.g.

sqlcmd create mssql --use https://aka.ms/AdventureWorksLT.bak,adventure_works

This will result in the restored database being named [adventure_works] instead of [AdventureWorksLT]

  1. --use now supports .mdf/.bacpac and .bak files
    sqlcmd create mssql --use [.mdf | .bacpac | *.bak]

  2. --use now supports compressed file formats (this also allows a .mdf/.ldf file pair)
    sqlcmd create mssql --use [.7z | .zip | .tar] # containing any of the above (inc. a .mdf/.ldf pair)

  3. the new sqlcmd use subcommand allows all of the above, to add a database to an already existing container
    sqlcmd use [<local file>|<remote file>]

This PR also enables the open sub-command e.g. sqlcmd open ads to be leveraged directly in sqlcmd create, e.g.

sqlcmd create mssql --use https://aka.ms/AdventureWorksLT.bak --open ads --open-file https://aka.ms/AdventureWorksLT.sql

This enables a T/SQL repro (including database and T/SQL script file) to be shared in a single command line

Examples:

sqlcmd install mssql --accept-eula

# Restore a backup (override database name to be [adventure_works], instead of [AdventureWorksLT])
sqlcmd use https://aka.ms/AdventureWorksLT.bak,adventure_works
sqlcmd use c:\Users\alias\Downloads\AdventureWorksLT.bak,adventure_works

# Restore a Dacfx .bacpac
sqlcmd use https://github.com/Microsoft/sql-server-samples/releases/download/wide-world-importers-v1.0/WideWorldImportersDW-Full.bacpac

# Attach a compressed .mdf/.ldf
sqlcmd use https://downloads.brentozar.com/StackOverflow2010.7z

@stuartpa stuartpa force-pushed the stuartpa/sqlcmd-urls branch from b239523 to 0e6223f Compare April 6, 2023 10:38
@shueybubbles
Copy link
Collaborator

shueybubbles commented Apr 6, 2023

What about supporting non-public URLs (SAS etc)? Does it download the file then discover its type similar to how you'd have to examine a zip to know its content?


endpoint, user := config.CurrentContext()

c.sql = sql.New(sql.SqlOptions{UnitTesting: false})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnitTesting: false}

isn't false the default value?
We shouldn't need to have product code give hints about unit testing. Test hooks should be available for tests to inject things.

output.FatalfWithHintExamples([][]string{
{"Create new context with a sql container ", "sqlcmd create mssql"},
}, "Current context does not have a container")
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what will this look like for non-container connections?
We can't claim this is production/enterprise ready until we support that.

I'd have expected most of the code from lines 76 to 96 to be outside the "if it's a container block"

Supporting a UNC or local file path to a bak file is a relatively straightforward restore operation

Restore from URL is also straightforward without copying the file locally if the server has a credential stored for the URL already.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is only for container scenarios (I'll update the PR description). I am not sure if non-container scenarios are needed. (folks already have solutions for all of this). Containers are particularly tricky in this regard for our normal users (who are windows centric). This functionality abstracts most of the users during the early part of their journey from all these internals.

"Unable to download file to container")
}

useDatabase.CopyToContainer(id)
Copy link
Collaborator

@shueybubbles shueybubbles Apr 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CopyToContainer(id)

for .bak files using a URL we should provide a way to create a CREDENTIAL to store on the server so it can restore directly from the URL

SQL 2022 supports both https and s3 URLs #Resolved

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an issue for this:

#332

useDatabase.BringOnline(
c.sql.Query,
user.BasicAuth.Username,
secret.Decode(user.BasicAuth.Password, user.BasicAuth.PasswordEncryption),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have a "credential" or "authentication" abstraction associated with the user to pass around instead of explicit user name and password? We're going to need to support all the auth types.


// Create and add some files to the archive.
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tw := tar.NewWriter(&buf)

instead of having tw.Close below, shouldn't there be a "defer tw.Close()" call right after this? Nobody ever error checks a Close afaik

import "net/url"

type Uri struct {
uri string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uri string

what's the benefit of keeping the string around? can't we get it from the URL?

func (u Uri) IsLocal() bool {
if len(u.Scheme()) > 2 {
return false
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else {

no else needed here or on line 21

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couldn't this just be
return len(u.Scheme()) <= 2


// actualUrl returns the url without the query string
func (u Uri) ActualUrl() string {
terminator := strings.LastIndex(u.uri, ",")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

,

I think the comment is inaccurate. The query string is delimited by ?

func (u Uri) Filename() string {
filename := filepath.Base(u.ActualUrl())
if filename == "" {
panic("filename is empty")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

panic("filename is empty")

this reliance on file name in the user's URL seems very limiting, even at this phase of our development.
It's fine for the Uri to say "I don't know the file name" then let the consumer decide what to do. The consumer might say "ok let's download it and see what we get" or "fail if the user didn't tell us what mechanism to use".
Presumably if the mechanism is "restore" we can assume it's a "bak", correct?

return ""
}

func (u Uri) GetDbNameAsIdentifier() string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

etDbNameAsIdenti

these SQL query escaping functions don't belong here.
They should be in their own package for general purpose use to safely inject identifiers into format strings.

}

tests := []test{
{"https://example.com/testdb.bak,myDbName", "myDbName"},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  {"https://example.com/testdb.bak,myDbName" [](http://example.com/codeflow?start=0&length=44)

It's a bit odd to conflate "URL + a database name" as "Uri"

The fact that --use flag and "use" command take this format as a parameter is specific to the semantic of those commands, correct?
I would expect these strings to be deserialized into something more meaningful like

type DatabaseFile struct {
  Source *url.URL
  DatabaseName string
}

{"https://example.com/test.foo", "test"},
{"https://example.com/test.foo,test", "test"},
{"https://example.com/test.7z,tql_name", "tsql_name"},
{"https://example.com/test.mdf,tsql_name?foo=bar", "tsql_name"},
Copy link
Collaborator

@shueybubbles shueybubbles Apr 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"https://example.com/test.mdf,tsql_name?foo=bar

how does putting the query portion of the URL after the comma work? It doesn't look like valid input.

A URL with a query would be

https://example.com/test.mdf?version=10,mydatabase

return false
} else {
return true
}
Copy link
Collaborator

@shueybubbles shueybubbles Apr 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose we pick one of three styles to avoid superfluous "else" blocks

1

return i.extractor != nil

2

func (i *ingest) IsExtractionNeeded() needed bool {
i.extractor = extract.NewExtractor(i.uri.FileExtension(), i.controller)
needed = iExtractor != nil
}

3

func (i *ingest) IsExtractionNeeded() bool {
 i.extractor = extract.NewExtractor(i.uri.FileExtension(), i.controller)
 if i.extractor == nil {
  return false
 }
  return true
}

My vote is 1 .

i.options.Filename = i.uri.Filename()

if i.options.Filename == "" {
panic("filename is empty")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ename is empty")

if the user provides a mechanism but the URL doesn't have a detectable file name, can we just generate a unique name with an extension that matches the mechanism?


alterDefaultDb := fmt.Sprintf(
"ALTER LOGIN [%s] WITH DEFAULT_DATABASE = [%s]",
username,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

username,

user names need to be bracket escaped as well.
Fundamentally, we need a package with the same helper functions SMO has for safe scripting. See https://github.com/microsoft/sqlmanagementobjects/blob/54e95cd095e232e43e711e65a83d55514ddc5e0e/src/Microsoft/SqlServer/Management/Smo/SqlSmoObject.cs#L329

@@ -0,0 +1,17 @@
package uri
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add the copyright header to your new files

func (e *sevenZip) IsInstalled(containerId string) bool {
e.containerId = containerId

return false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return false

tbd?

"wget",
"-O",
"/opt/7-zip/7-zip.tar",
"https://7-zip.org/a/7z2201-linux-x64.tar.xz"})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x64

don't we need to get the architecture at runtime depending on where sqlcmd is running?

containerId string
}

func (e *sevenZip) Initialize(controller *container.Controller) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initialize(controller *container.Controller)

How can we abstract away the differences between
"SQL running in a container", "SQL running on the local host", "SQL running in the cloud", and "SQL running on a remote host"?
Or would we have multiple extractor implementations and this one should be renamed "sevenZipInLinuxX64Container" and we'd have others like "windowsZipLocalHost" ?

I don't see how this model evolves to support the non-container scenarios.

@shueybubbles
Copy link
Collaborator

shueybubbles commented Apr 6, 2023

Could you create a couple mermaid diagrams in the wiki? One would show the relationship between components like ingest and extract and container, and one would show how the contents of the bak/ldf/dacpac file would move from their source to the database for each environment we support (sql in a container, sql on localhost, sql on a remote host, sql in the cloud).

graph TD;
a -- label --> b
Loading

}

func extractPaths(input string) []string {
re := regexp.MustCompile(`Path\s*=\s*(\S+)`)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`Path

i assume this is never localized


paths := extractPaths(stdout)

fmt.Println(paths)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add some asserts

uri: uri,
controller: controller,
}
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove else

m.setFilePermissions(m.CopyToLocation() + "/" + options.Filename)
if options.LdfFilename == "" {
text += `CREATE DATABASE [%s] ON (FILENAME = '%s/%s') FOR ATTACH;`
query(fmt.Sprintf(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

query(fmt.Sprintf

you need to escape the parameters properly, and use nvarchar for the file name

eg from SMO

ddl.AppendFormat(SmoApplication.DefaultCulture, ", FILENAME = N'{0}' ", SqlString(fileName));

        public static String SqlString(String s)
        {
            return EscapeString(s, '\'');
        }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'd be a good test case to have some file names that use extended characters. Go stores strings as UTF8 but nvarchar is UTF16, while varchar is SQL-collation-dependent.
How do we guarantee the Go strings get encoded properly in the query?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe go-mssqldb does the right thing already for strings and we should use a parameterized query to let the driver do the conversion instead of manually inserting tokens in the query.

Password string

Filename string
LdfFilename string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LdfFilename string

should this be an array?


m.controller.DownloadFile(
m.containerId,
"https://aka.ms/sqlpackage-linux",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/sqlpackage-linux",

is sqlpackage CPU architecture agnostic? Does this also bring down Net Core runtime?

@dlevy-msft dlevy-msft added this to the January 2024 Release milestone Dec 7, 2023
stuartpa added a commit to stuartpa/dab-swa-todo that referenced this pull request Dec 29, 2023
Simplifying the getting started README for when this sqlcmd PR is pushed

microsoft/go-sqlcmd#319
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants